package com.black.utils.office.excel;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.black.utils.file.FileTypeEnum;
import com.opencsv.CSVReader;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static com.black.utils.office.excel.ExcelExportUtil.getCell;
import static com.black.utils.office.excel.ExcelExportUtil.getRow;


/**
 * @author ylx
 */
@Slf4j
public class ExcelLoadUtil {
    /**
     * 根据文件名加载对应格式的文件内容到JSONObject。
     *
     * @param fileName 文件名，包括文件路径和后缀名。
     * @return 返回一个JSONObject，包含文件的加载结果。
     * @throws RuntimeException 如果文件类型不受支持，则抛出运行时异常。
     */
    public static List<ExcelEntity> load(String fileName) throws IOException{
        // 从文件名中确定导出的文件格式。
        FileTypeEnum type = FileTypeEnum.getType(fileName);
        // 根据文件后缀名选择加载方法
        switch (type) {
            case XLS:
            case XLSX:
                // 加载Excel文件
                return loadExcel(fileName);
            case CSV:
                // 加载CSV文件
                return loadCsv(fileName);
            default:
                // 如果文件类型不受支持，则抛出异常
                throw new RuntimeException("不支持的文件类型;Unsupported file type");
        }
    }

    /**
     * 从Excel文件中加载数据并将其转换为JSONObject格式。
     * 此方法处理Excel文件中的每个工作表，将每个工作表的数据转换为JSONArray，
     * 然后将所有工作表的数据存储在一个大的JSONObject中，其中键是工作表的索引。
     *
     * @param fileName 要加载的Excel文件的名称。
     * @return 包含所有工作表数据的JSONObject。
     * @throws RuntimeException 如果读取Excel文件时发生IO异常。
     */
    private static List<ExcelEntity> loadExcel(String fileName) throws IOException{
        List<ExcelEntity> data = new ArrayList<>();
        try (Workbook workbook = WorkbookFactory.create(new File(fileName))) {
            int numberOfSheets = workbook.getNumberOfSheets();
            log.info("{}文件共有{}个工作表", fileName, numberOfSheets);
            // 遍历所有工作表，将每个工作表的数据转换为JSONArray
            for (int i = 0; i < numberOfSheets; i++) {
                Sheet sheet = workbook.getSheetAt(i);
                // 获取当前Sheet中的数据。
                log.info("load row");
                JSONArray rowList = load(sheet, 0, sheet.getPhysicalNumberOfRows());
                // 将当前工作表的数据添加到JSONObject中，键为工作表索引
                ExcelEntity excelEntity = new ExcelEntity();
                excelEntity.setData(rowList);
                log.info("generate headers by columnNumber");
                excelEntity.setHeaders(generateHeaders(rowList.getJSONObject(0)));
                data.add(excelEntity);
            }
        }
        return data;
    }

    /**
     * 从CSV文件加载数据并转换为ExcelEntity列表。
     * 这个方法主要用于解析CSV文件中的数据，将其转换为内部表示形式，方便进一步处理或导出为Excel格式。
     *
     * @param fileName CSV文件的名称。这个参数指定了解析数据的来源。
     * @return 返回一个包含ExcelEntity的列表，每个ExcelEntity代表CSV文件中的一行数据。
     * @throws RuntimeException 如果在读取CSV文件或解析数据时发生IO异常，则抛出运行时异常。
     */
    private static List<ExcelEntity> loadCsv(String fileName) {
        try (CSVReader reader = new CSVReader(new FileReader(fileName))) {
            List<ExcelEntity> data = new ArrayList<>();
            Iterator<String[]> iterator = reader.iterator();
            log.info("load row");
            JSONArray rowList = load(iterator);
            ExcelEntity excelEntity = new ExcelEntity();
            excelEntity.setData(rowList);
            log.info("generate headers by columnNumber");
            excelEntity.setHeaders(generateHeaders(rowList.getJSONObject(0)));
            data.add(excelEntity);
            return data;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据文件名和文件类型，加载带有头部的文件内容。
     * 此方法根据文件的后缀名决定使用哪种方式加载文件，支持的文件类型包括Excel（.xls、.xlsx）和CSV（.csv）。
     * 对于Excel文件，它会读取指定的最后一行作为头部，而对于CSV文件，它假设文件的第一行是头部。
     *
     * @param fileName      文件名，包括文件路径和后缀名。
     * @param lastHeaderRow Excel文件中最后一行的头部，这个参数对CSV文件无效。
     * @return 返回一个JSONObject，其中包含了文件的内容。
     * @throws RuntimeException 如果文件类型不受支持，则抛出运行时异常。
     */
    public static List<ExcelEntity> loadWithHeader(String fileName, int lastHeaderRow) throws IOException{
        // 从文件名中确定加载文件格式。
        FileTypeEnum type = FileTypeEnum.getType(fileName);
        // 根据文件的后缀名选择合适的加载方法。
        switch (type) {
            case XLS:
            case XLSX:
                // 加载Excel文件，考虑到可能是.xls或.xlsx格式。
                return loadExcelWithHeader(fileName, lastHeaderRow);
            case CSV:
                // 加载CSV文件。
                return loadCsvWithHeader(fileName);
            default:
                // 如果文件类型不受支持，则抛出异常。
                throw new RuntimeException("不支持的文件类型;Unsupported file type");
        }
    }

    /**
     * 从Excel文件中加载数据，并解析表头信息。
     *
     * @param fileName Excel文件名。
     * @param last     指定最后一行作为数据行的偏移量，用于处理非标准表头的情况。
     *                 即使是数据行也会被归为表头, 数据行避免表头名冲突,采用列名作为key。
     * @return 返回一个ExcelEntity列表，其中包含每个工作表的表头信息和数据。
     * @throws RuntimeException 如果读取Excel文件发生IO错误，则抛出运行时异常。
     */
    private static List<ExcelEntity> loadExcelWithHeader(String fileName, int last) throws IOException{
        List<ExcelEntity> res = new ArrayList<>();
        try (Workbook workbook = WorkbookFactory.create(new File(fileName))) {
            // 获取工作簿中的工作表数量
            log.info("{}文件共有{}个工作表", fileName, workbook.getNumberOfSheets());
            int numberOfSheets = workbook.getNumberOfSheets();
            // 遍历每个工作表
            for (int i = 0; i < numberOfSheets; i++) {
                Sheet sheet = workbook.getSheetAt(i);
                ExcelEntity excelEntity = new ExcelEntity();
                // 初始化最大表头行和列的索引
                int lastHeaderRow = last;
                int lastHeaderColumn = 0;
                // 用于存储所有表头信息
                Set<ExcelHeader> allHeaders = new HashSet<>();
                // 获取并处理工作表中的合并单元格区域
                log.info("{}工作表共有{}个合并单元格", sheet.getSheetName(), sheet.getNumMergedRegions());
                List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
                for (CellRangeAddress mergedRegion : mergedRegions) {
                    // 获取合并单元格的起始行和列索引
                    int firstRow = mergedRegion.getFirstRow();
                    int firstColumn = mergedRegion.getFirstColumn();
                    // 获取起始单元格
                    Row row = getRow(sheet, firstRow);
                    Cell cell = getCell(row, firstColumn);
                    // 创建并初始化表头信息
                    ExcelHeader header = new ExcelHeader(getValue(cell));
                    header.setRow(firstRow);
                    header.setColumn(firstColumn);
                    // 设置合并单元格的结束行和列索引
                    int lastRow = mergedRegion.getLastRow();
                    int lastColumn = mergedRegion.getLastColumn();
                    header.setLastRow(lastRow);
                    header.setLastColumn(lastColumn);
                    // 以列索引作为表头的唯一键
                    header.setKey("" + firstColumn);
                    // 更新最大表头行和列的索引
                    if (lastColumn > lastHeaderColumn) {
                        lastHeaderColumn = lastColumn;
                    }
                    if (lastRow > lastHeaderRow) {
                        lastHeaderRow = lastRow;
                    }
                    // 将表头信息添加到集合中
                    allHeaders.add(header);
                }
                log.info("获取全部非空表头");
                // 处理非合并单元格的表头信息
                for (int j = 0; j <= lastHeaderRow; j++) {
                    Row row = getRow(sheet, j);
                    for (int k = 0; k <= lastHeaderColumn; k++) {
                        Cell cell = getCell(row, k);
                        if (cell != null) {
                            String value = getValue(cell);
                            // 忽略空白单元格
                            if (StringUtils.isBlank(value)) {
                                continue;
                            }
                            ExcelHeader header = new ExcelHeader(value);
                            header.setRow(j);
                            header.setColumn(k);
                            header.setLastRow(j);
                            header.setLastColumn(k);
                            // 以列索引作为表头的唯一键
                            header.setKey("" + k);
                            // 将表头信息添加到集合中
                            allHeaders.add(header);
                        }
                    }
                }
                // 筛选并排序位于第一行的表头
                List<ExcelHeader> headers = allHeaders.stream().filter(header -> header.getRow() == 0).sorted(Comparator.comparingInt(ExcelHeader::getColumn)).collect(Collectors.toList());
                // 构建表头的层级关系
                for (ExcelHeader header : headers) {
                    log.info("根据合并单元格构建表头层级");
                    buildHeaders(header, allHeaders, lastHeaderRow);
                }
                // 加载并处理工作表中的数据行
                JSONArray data = load(sheet, lastHeaderRow + 1, sheet.getPhysicalNumberOfRows());
                // 设置表头信息和数据到Excel实体中
                excelEntity.setHeaders(headers);
                excelEntity.setData(data);
                excelEntity.setSheetName(sheet.getSheetName());
                // 将Excel实体添加到结果集合中
                res.add(excelEntity);
            }
        }
        return res;
    }

    /**
     * 从指定的CSV文件中加载数据，该文件应包含头部信息。
     * 使用CSVReader读取文件，将数据转换为JSONObject格式返回，其中包含Excel数据的结构和内容。
     *
     * @param fileName CSV文件的名称，用于创建CSVReader以读取文件。
     * @return 包含CSV文件结构和数据的JSONObject。
     * @throws RuntimeException 如果在读取文件或处理CSV数据时发生IO异常，则抛出运行时异常。
     */
    private static List<ExcelEntity> loadCsvWithHeader(String fileName) {
        try (CSVReader reader = new CSVReader(new FileReader(fileName))) {
            List<ExcelEntity> res = new ArrayList<>();
            ExcelEntity excelEntity = new ExcelEntity();
            // 获取CSV文件的头部行，用于创建ExcelHeader对象，定义数据结构。
            Iterator<String[]> iterator = reader.iterator();
            String[] headerString = iterator.next();
            // 创建ExcelHeader列表，用于存储CSV文件的头部信息。
            List<ExcelHeader> headers = new ArrayList<>();
            for (int i = 0; i < headerString.length; i++) {
                // 根据头部列名创建ExcelHeader对象，并设置唯一的键值（基于列索引）。
                ExcelHeader header = new ExcelHeader(headerString[i]);
                header.setKey("" + i);
                headers.add(header);
            }
            // 将头部信息设置到ExcelEntity对象中。
            excelEntity.setHeaders(headers);
            // 加载CSV文件的数据部分，并将其存储到JSONArray中。
            JSONArray data = load(iterator);
            // 将数据设置到ExcelEntity对象中。
            excelEntity.setData(data);
            // 将ExcelEntity对象添加到JSONObject中并返回。
            res.add(excelEntity);
            return res;
        } catch (IOException e) {
            // 在处理IO异常时，抛出运行时异常。
            throw new RuntimeException(e);
        }
    }

    /**
     * 从Excel的Sheet中加载数据，并将其转换为JSONArray格式。
     * 此方法主要处理Excel的行级数据读取和转换，不涉及列的特定逻辑。
     *
     * @param sheet    Excel中的工作表，从中读取数据。
     * @param startRow 起始读取的行号，包含该行。
     * @param endRow   结束读取的行号，不包含该行。
     * @return 返回一个JSONArray，其中包含了从Excel中读取的所有行数据。
     */
    public static JSONArray load(Sheet sheet, int startRow, int endRow) {
        log.info("加载Excel数据{}~{}行", startRow, endRow);
        JSONArray data = new JSONArray(); // 初始化用于存储数据的JSONArray
        // 遍历指定范围的行，从startRow到endRow-1
        for (int j = startRow; j < endRow; j++) {
            JSONObject rowItem = new JSONObject(); // 创建一个JSONObject，用于存储当前行的数据
            Row row = getRow(sheet, j); // 获取当前行
            if (row != null) {
                // 获取当前行的物理单元格数量
                // 获取当前行的数据。
                int physicalNumberOfCells = row.getPhysicalNumberOfCells();
                // 遍历当前行的所有单元格
                for (int k = 0; k < physicalNumberOfCells; k++) {
                    // 获取当前单元格
                    // 获取当前单元格的数据。
                    Cell cell = getCell(row, k);
                    String value = getValue(cell); // 获取单元格的值
                    rowItem.put("" + k, value); // 将单元格的值添加到当前行的JSONObject中
                }
            }
            data.add(rowItem); // 将当前行的JSONObject添加到数据数组中
        }
        return data; // 返回处理后的数据数组
    }

    /**
     * 将迭代器中的字符串数组转换为JSON数组。
     * 此方法遍历给定的字符串数组迭代器，并将每个字符串数组转换为一个JSONObject，
     * 然后将这些JSONObject添加到一个JSON数组中。
     * 这种转换有助于将二维数据结构表示为更灵活的JSON格式，适用于数据传输或存储。
     *
     * @param iterator 字符串数组的迭代器，每个字符串数组代表一行数据。
     * @return 包含转换后的JSONObject的JSON数组。
     */
    public static JSONArray load(Iterator<String[]> iterator) {
        // 初始化一个JSON数组，用于存储转换后的数据。
        JSONArray data = new JSONArray();
        // 当迭代器中有更多元素时，循环处理每个字符串数组。
        while (iterator.hasNext()) {
            // 获取并处理迭代器的下一个字符串数组。
            String[] next = iterator.next();
            // 初始化一个JSONObject，用于存储当前字符串数组的数据。
            JSONObject row = new JSONObject();
            // 遍历当前字符串数组，并将每个元素作为键值对添加到JSONObject中。
            // 键是元素的索引，值是元素本身的值。
            for (int i = 0; i < next.length; i++) {
                row.put("" + i, next[i]);
            }
            // 将当前的JSONObject添加到JSON数组中。
            data.add(row);
        }
        // 返回包含所有转换后的数据的JSON数组。
        return data;
    }

    /**
     * 根据给定的Excel单元格获取其值，并以字符串形式返回。
     *
     * @param cell Excel中的单元格对象。
     * @return 单元格的值，如果单元格为null，则返回null。
     */
    public static String getValue(Cell cell) {
        // 检查单元格是否为空，如果为空则直接返回null
        if (cell == null) {
            return null;
        }
        // 根据单元格的类型来获取其值
        switch (cell.getCellType()) {
            case STRING:
                // 如果单元格类型为字符串，则直接返回字符串值
                return cell.getStringCellValue();
            case NUMERIC:
                // 如果单元格类型为数值，则将数值转换为字符串返回
                // poi 对数值都会处理为double, 所以这里需要转换整数
                double numericCellValue = cell.getNumericCellValue();
                if (Math.floor(numericCellValue) == numericCellValue) {
                    return String.valueOf((int) numericCellValue);
                }
                return String.valueOf(numericCellValue);
            case BOOLEAN:
                // 如果单元格类型为布尔值，则将布尔值转换为字符串返回
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                // 如果单元格类型为公式，需要计算公式结果并根据结果的类型返回相应的值
                FormulaEvaluator evaluator = cell.getSheet().getWorkbook().getCreationHelper().createFormulaEvaluator();
                CellValue cellValue = evaluator.evaluate(cell);
                switch (cellValue.getCellType()) {
                    case STRING:
                        // 如果公式结果为字符串，则返回字符串值
                        return cellValue.getStringValue();
                    case NUMERIC:
                        // 如果公式结果为数值，则将数值转换为字符串返回
                        double formulaValue = cellValue.getNumberValue();
                        if (Math.floor(formulaValue) == formulaValue) {
                            return String.valueOf((int) formulaValue);
                        }
                        return String.valueOf(formulaValue);
                    case BOOLEAN:
                        // 如果公式结果为布尔值，则将布尔值转换为字符串返回
                        return String.valueOf(cellValue.getBooleanValue());
                    case ERROR:
                        // 如果公式结果为错误，则返回错误代码的字符串表示
                        return String.valueOf(cellValue.getErrorValue());
                    default:
                        // 如果公式结果为未知类型，则返回null
                        return null;
                }
            case BLANK:
                // 如果单元格为空，则返回空字符串
                return "";
        }
        // 如果单元格类型未被识别，则返回null
        return null;
    }

    /**
     * 构建Excel表格的头部结构。
     * 该方法通过递归方式将头部结构拆分成更小的子头部，直到最后一行头部。
     * 这样做是为了更好地组织和处理Excel表格的头部信息，便于后续的数据解析和处理。
     *
     * @param header        当前处理的头部单元格。
     * @param allHeaders    所有头部单元格的集合，用于查找当前头部的子头部。
     * @param lastHeaderRow 最后一行头部的行号，用于判断是否结束递归。
     */
    private static void buildHeaders(ExcelHeader header, Set<ExcelHeader> allHeaders, int lastHeaderRow) {
        log.info("构建层级表头");
        // 获取当前头部单元格的列号、最后一行的行号和最后一列的列号
        int column = header.getColumn();
        int lastRow = header.getLastRow();
        int lastColumn = header.getLastColumn();
        // 如果当前头部单元格的最后一行等于设定的最后头部行号，则不再递归处理
        if (lastRow == lastHeaderRow) {
            return;
        }
        // 通过流式处理找到所有位于当前头部单元格下一行、且列范围覆盖当前头部的单元格，这些将作为当前头部的子头部
        List<ExcelHeader> subHeaders = allHeaders.stream()
                .filter(h -> h.getRow() == lastRow + 1 && h.getColumn() >= column && h.getLastColumn() <= lastColumn)
                .sorted(Comparator.comparingInt(ExcelHeader::getColumn)) // 按照列号排序，确保子头部的顺序与表格一致
                .collect(Collectors.toList());
        // 将找到的子头部设置给当前头部，建立父子头部的关系
        header.setChildren(subHeaders);
        // 对每个子头部递归调用本方法，继续拆分和构建更细粒度的头部结构
        for (ExcelHeader subHeader : subHeaders) {
            buildHeaders(subHeader, allHeaders, lastHeaderRow);
        }
    }

    /**
     * 按照列号生成一次表头
     *
     * @param row 首行数据
     * @return 表头列表
     */
    private static List<ExcelHeader> generateHeaders(JSONObject row) {
        int columnSize = row.size();
        // 初始化表头列表
        List<ExcelHeader> headers = new ArrayList<>();
        // 遍历第一行的每一列，创建并添加ExcelHeader对象到列表中
        for (int i = 0; i < columnSize; i++) {
            // 列名暂时使用列的索引值
            String name = String.valueOf(i);
            // 创建一个新的ExcelHeader对象
            ExcelHeader header = new ExcelHeader(name);
            // 设置列名（这里使用索引值作为列名）
            header.setKey(name);
            // 将ExcelHeader对象添加到表头列表中
            headers.add(header);
        }
        return headers;
    }
}
